/*
 * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.jse.server;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.net.HttpCookie;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.StandardSocketOptions;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.net.ssl.SSLSession;
import com.jse.Http;
import com.jse.Io;
import com.jse.Lang;
import com.jse.json.Json;
import com.jse.json.JsonObject;
import com.jse.json.XML;
import com.jse.multipart.MultipartFormData;
import com.jse.server.websockets.WebSocketHandler;
import com.jse.web.Ctx;
import com.jse.web.Web;

public class HttpExchange extends HashMap<String,Object> implements Ctx,AutoCloseable {

	Headers reqHd, resHd;
    Request req;
    String method;
    URI uri;
    HttpConnection connection;
    long reqContentLen;
    long rspContentLen;
    /* raw streams which access the socket directly */
    InputStream ris;
    OutputStream ros;
    /* close the underlying connection when this exchange finished */
    boolean close;
    boolean closed;
    boolean http10 = false;
    private String path;
    private String query;
    private String suffix;
    private String serverName;
    private String mapping;

    /* for formatting the Date: header */
    private static final DateTimeFormatter FORMATTER;
    static {
        String pattern = "EEE, dd MMM yyyy HH:mm:ss zzz";
        FORMATTER = DateTimeFormatter.ofPattern(pattern, Locale.US)
                .withZone(ZoneId.of("GMT"));
    }

    private static final String HEAD = "HEAD";

    /*
     * streams which take care of the HTTP protocol framing
     * and are passed up to higher layers
     */
    InputStream uis;
    OutputStream uos;
    LeftOverInputStream uis_orig; // uis may have be a user supplied wrapper
    PlaceholderOutputStream uos_orig;

    /* true after response headers sent */
    volatile boolean sentHeaders;

    Map<String, Object> attributes;
    int rcode = -1;
    final boolean websocket;

    public HttpExchange(
            String m, URI u, Request req, long len, HttpConnection connection) throws IOException {
        this.req = req;
        // make a mutable copy to allow HttpHandler to modify in chain
        this.reqHd = new Headers(req.headers());
        this.resHd = new Headers();
        this.method = m;
        this.uri = u;
        this.connection = connection;
        this.websocket = WebSocketHandler.isWebsocketRequested(this.reqHd);
        if (this.websocket) {
            // length is indeterminate
            len = -1;
        }
        this.reqContentLen = len;
        /* ros only used for headers, body written directly to stream */
        this.ros = req.outputStream();
        this.ris = req.inputStream();
        path=u.getPath();
		int suffixindex=path.lastIndexOf(".");
		suffix=suffixindex==-1?"":path.substring(suffixindex+1);
		serverName=connection.getSocket().getInetAddress().getHostName();
		query=u.getQuery();
    }

    public Headers getRequestHeaders() {
        return reqHd;
    }

    public Headers getResponseHeaders() {
        return resHd;
    }

    public URI getRequestURI() {
        return uri;
    }

    public String getRequestMethod() {
        return method;
    }

    public HttpContext getHttpContext() {
        return connection.getHttpContext();
    }

    private boolean isHeadRequest() {
        return HEAD.equals(getRequestMethod());
    }

    public void close() {
        if (closed) {
            return;
        }
        closed = true;

        /*
         * close the underlying connection if,
         * a) the streams not set up yet, no response can be sent, or
         * b) if the wrapper output stream is not set up, or
         * c) if the close of the input/outpu stream fails
         */
        try {
            if (uis_orig == null || uos == null) {
                connection.close();
                return;
            }
            if (!uos_orig.isWrapped()) {
                connection.close();
                return;
            }
            if (!uis_orig.isClosed()) {
                uis_orig.close();
            }
            uos.close();
        } catch (IOException e) {
            connection.close();
        }
    }

    public InputStream getRequestBody() {
        if (uis != null) {
            return uis;
        }
        if (websocket) {
            uis_orig = new UndefLengthInputStream(this, ris);
            uis = uis_orig;
        } else if (reqContentLen == -1L) {
            uis_orig = new ChunkedInputStream(this, ris);
            uis = uis_orig;
        } else {
            uis_orig = new FixedLengthInputStream(this, ris, reqContentLen);
            uis = uis_orig;
        }
        return uis;
    }

    LeftOverInputStream getOriginalInputStream() {
        return uis_orig;
    }

    public int getResponseCode() {
        return rcode;
    }

    public OutputStream getResponseBody() {
        /*
         * TODO. Change spec to remove restriction below. Filters
         * cannot work with this restriction
         *
         * if (!sentHeaders) {
         * throw new IllegalStateException ("headers not sent");
         * }
         */
        if (uos == null) {
            uos_orig = new PlaceholderOutputStream(null);
            uos = uos_orig;
        }
        return uos;
    }

    /*
     * returns the place holder stream, which is the stream
     * returned from the 1st call to getResponseBody()
     * The "real" ouputstream is then placed inside this
     */
    PlaceholderOutputStream getPlaceholderResponseBody() {
        getResponseBody();
        return uos_orig;
    }

    public void sendResponseHeaders(int rCode, long contentLen)
            throws IOException {
        final Logger logger = getServer().getLogger();
        if (sentHeaders) {
            throw new IOException("headers already sent");
        }
        this.rcode = rCode;
        String statusLine = rCode == 101 ? "HTTP/1.1 101 Switching Protocols\r\n"
                : "HTTP/1.1 " + rCode + Http.msg(rCode) + "\r\n";
        OutputStream tmpout = new BufferedOutputStream(ros);
        PlaceholderOutputStream o = getPlaceholderResponseBody();
        tmpout.write(bytes(statusLine, 0), 0, statusLine.length());
        boolean noContentToSend = false; // assume there is content
        boolean noContentLengthHeader = false; // must not send Content-length is set
        resHd.set("Date", FORMATTER.format(Instant.now()));

        if (this.getAttribute("__SOCKET_WRITE_BUFFER") != null) {
            int bufferSize = (Integer) this.getAttribute("__SOCKET_WRITE_BUFFER");
            getConnection().getSocket().setOption(StandardSocketOptions.SO_SNDBUF, bufferSize);
        }

        /* check for response type that is not allowed to send a body */
        if (rCode == 101) {
            logger.log(Level.INFO, "switching protocols");

            if (contentLen != 0) {
                String msg = "sendResponseHeaders: rCode = " + rCode
                        + ": forcing contentLen = 0";
                logger.log(Level.WARNING, msg);
            }
            contentLen = 0;

        } else if ((rCode >= 100 && rCode < 200) /* informational */
                || (rCode == 204) /* no content */
                || (rCode == 304)) /* not modified */
        {
            if (contentLen != -1) {
                String msg = "sendResponseHeaders: rCode = " + rCode
                        + ": forcing contentLen = -1";
                logger.log(Level.WARNING, msg);
            }
            contentLen = -1;
            noContentLengthHeader = (rCode != 304);
        }

        if (isHeadRequest() || rCode == 304) {
            /*
             * HEAD requests or 304 responses should not set a content length by passing it
             * through this API, but should instead manually set the required
             * headers.
             */
            if (contentLen >= 0) {
                String msg = "sendResponseHeaders: being invoked with a content length for a HEAD request";
                logger.log(Level.WARNING, msg);
            }
            noContentToSend = true;
            contentLen = 0;
            o.setWrappedStream(new FixedLengthOutputStream(this, ros, contentLen));
        } else { /* not a HEAD request or 304 response */
            if (contentLen == 0) {
                if (http10 || websocket) {
                    o.setWrappedStream(new UndefLengthOutputStream(this, ros));
                    close = true;
                } else {
                    resHd.set("Transfer-encoding", "chunked");
                    o.setWrappedStream(new ChunkedOutputStream(this, ros));
                }
            } else {
                if (contentLen == -1) {
                    noContentToSend = true;
                    contentLen = 0;
                }
                if (!noContentLengthHeader) {
                    resHd.set("Content-length", Long.toString(contentLen));
                }
                o.setWrappedStream(new FixedLengthOutputStream(this, ros, contentLen));
            }
        }

        // A custom handler can request that the connection be
        // closed after the exchange by supplying Connection: close
        // to the response header. Nothing to do if the exchange is
        // already set up to be closed.
        if (!close) {
            Stream<String> conheader = Optional.ofNullable(resHd.get("Connection"))
                    .map(List::stream).orElse(Stream.empty());
            if (conheader.anyMatch("close"::equalsIgnoreCase)) {
                logger.log(Level.DEBUG, "Connection: close requested by handler");
                close = true;
            }
        }

        write(resHd, tmpout);
        this.rspContentLen = contentLen;
        tmpout.flush();
        tmpout = null;
        sentHeaders = true;
        logger.log(Level.TRACE, "Sent headers: noContentToSend=" + noContentToSend);
        if (noContentToSend) {
            close();
        }
        getServer().logReply(rCode, req.requestLine(), null);
    }

    void write(Headers map, OutputStream os) throws IOException {
        Set<Map.Entry<String, List<String>>> entries = map.entrySet();
        for (Map.Entry<String, List<String>> entry : entries) {
            String key = entry.getKey();
            byte[] buf;
            List<String> values = entry.getValue();
            for (String val : values) {
                int i = key.length();
                buf = bytes(key, 2);
                buf[i++] = ':';
                buf[i++] = ' ';
                os.write(buf, 0, i);
                buf = bytes(val, 2);
                i = val.length();
                buf[i++] = '\r';
                buf[i++] = '\n';
                os.write(buf, 0, i);
            }
        }
        os.write('\r');
        os.write('\n');
    }

    private byte[] rspbuf = new byte[128]; // used by bytes()

    /**
     * convert string to byte[], using rspbuf
     * Make sure that at least "extra" bytes are free at end
     * of rspbuf. Reallocate rspbuf if not big enough.
     * caller must check return value to see if rspbuf moved
     */
    private byte[] bytes(String s, int extra) {
        int slen = s.length();
        if (slen + extra > rspbuf.length) {
            int diff = slen + extra - rspbuf.length;
            rspbuf = new byte[2 * (rspbuf.length + diff)];
        }
        char c[] = s.toCharArray();
        for (int i = 0; i < c.length; i++) {
            rspbuf[i] = (byte) c[i];
        }
        return rspbuf;
    }

    public InetSocketAddress getRemoteAddress() {
        Socket s = connection.getSocket();
        InetAddress ia = s.getInetAddress();
        int port = s.getPort();
        return new InetSocketAddress(ia, port);
    }

    public InetSocketAddress getLocalAddress() {
        Socket s = connection.getSocket();
        InetAddress ia = s.getLocalAddress();
        int port = s.getLocalPort();
        return new InetSocketAddress(ia, port);
    }

    public String getProtocol() {
        String reqline = req.requestLine();
        int index = reqline.lastIndexOf(' ');
        return reqline.substring(index + 1);
    }

    public SSLSession getSSLSession() {
        return connection.getSSLSession();
    }

    public Object getAttribute(String name) {
        if (name == null) {
            throw new NullPointerException("null name parameter");
        }
        if (attributes == null) {
            attributes = getHttpContext().getAttributes();
        }
        return attributes.get(name);
    }

    public void setAttribute(String name, Object value) {
        if (name == null) {
            throw new NullPointerException("null name parameter");
        }
        if (attributes == null) {
            attributes = getHttpContext().getAttributes();
        }
        if (value != null) {
            attributes.put(name, value);
        } else {
            attributes.remove(name);
        }
    }

    public void setStreams(InputStream i, OutputStream o) {
        assert uis != null;
        if (i != null) {
            uis = i;
        }
        if (o != null) {
            uos = o;
        }
    }

    /**
     * PP
     */
    HttpConnection getConnection() {
        return connection;
    }

    HttpServer getServer() {
        return getHttpContext().getServer();
    }
    
    private String body;
    private Map<String,HttpCookie> cookies;
    public int status=200;
    
    /**  body转为string @return String */
    public String body() {if(body!=null)return body;body=Io.readString(getRequestBody());return body;}
    public String getServerName() {return serverName;}
    public String getHeader(String name){return reqHd.getFirst(name);}
    public String responseHeader(String name){return resHd.getFirst(name);}
    public Map<String,HttpCookie> cookies() {
    	if(cookies!=null)return cookies;
    	cookies=new HashMap<>();
        String cookiestr = getHeader("Cookie");
        if (cookiestr==null||cookiestr.isEmpty()) {cookiestr = responseHeader("Set-Cookie");}
        if (cookiestr==null||cookiestr.isEmpty()) {return cookies;}
        String[] cookiearry = cookiestr.split(";");
        for(String ck : cookiearry){
        	String[] cks=ck.split("=");
        	cookies.put(cks[0],new HttpCookie(cks[0], cks[1]));
        }
        return cookies;
    }
    public HttpSession getSession(){
		String sid=getRequestedSessionId();
		if(sid==null||sid.isEmpty()){sid=Lang.snowflake()+"";}
		if(Web.SESSIONS.containsKey(sid))return (HttpSession)Web.SESSIONS.get(sid);
		HttpSession httpSession = new HttpSession(sid);
		Web.SESSIONS.put(sid,httpSession);
		return httpSession;
	}
	
	public String getRequestedSessionId() {var c=cookies().get("JSESSIONID");return c!=null?c.getValue():null;}
	private JsonObject tbl;
	
	public JsonObject tbl() {
		if(tbl!=null)return tbl;
		tbl=new JsonObject();
		parseTbl();
		return tbl;
	}
	private void parseTbl() {
		if("GET".equals(method)) {
			if(getRequestURI().getQuery()== null) {return;}
	        queryMap(getRequestURI().getQuery());
		}else if("POST".equals(method)) {
        	String reqcontentType=getRequestHeaders().getFirst("Content-Type");
        	if(reqcontentType==null) {//没有body none
        	}else if(reqcontentType.startsWith("application/x-www-form-urlencoded")) {
        		queryMap(body());
            }else if(reqcontentType.startsWith("application/json")) {
            	tbl.putAll(Json.jsonObject(body()));
            }else if(reqcontentType.startsWith("application/xml")) {
            	tbl.putAll(XML.toJSONObject(body()));
            }else if(reqcontentType.startsWith("multipart/form-data")) {//上传
            	try {
            		MultipartFormData formData = new MultipartFormData();
                	formData.parseRequestStream(getRequestBody(),StandardCharsets.UTF_8);
                	System.out.println(formData.getParamListMap());
                	System.out.println(formData.getFileListValueMap());
                	formData.getFile("fx").write("d:/1.png");
				} catch (IOException e) {
				}
            }else {
            	tbl.put("body",body());
            }
        }
	}
	private void queryMap(String query) {
		final String[] items = query.split("&");
		Arrays.stream(items).forEach(t ->{
            final String[] v = t.split("=");
            if( v.length == 2) {
                try{
                    final String key = URLDecoder.decode(v[0],"utf8");
                    final String val = URLDecoder.decode(v[1],"utf8");
                    tbl.add(key,val);
                }catch (UnsupportedEncodingException e) {}
            }
        });
	}

	@Override public int size() {return tbl().size();}
	@Override public boolean isEmpty() {return tbl().isEmpty();}
	@Override public boolean containsKey(Object key) {return tbl().containsKey(key);}
	@Override public boolean containsValue(Object value) {return tbl().containsValue(value);}
	@Override public Object get(Object key) {return tbl().get(key);}
	@Override public Object put(String key, Object value) {return tbl().put(key, value);}
	@Override public Object remove(Object key) {return tbl().remove(key);}
	@Override public void putAll(Map<? extends String, ? extends Object> m) {tbl().putAll(m);}
	@Override public void clear() {tbl().clear();}
	@Override public Set<String> keySet() {return tbl().keySet();}
	@Override public Collection<Object> values() {return tbl().values();}
	@Override public Set<Entry<String, Object>> entrySet() {return tbl().entrySet();}
	@Override public String toString() {return Json.toJson(this);}
	
	@Override public String getServletPath() {return path;}
	@Override public String getPath() {return path;}
	@Override public String getSuffix() {return suffix;}
	@Override public HttpCookie cookie(String name) {return cookies().get(name);}
	@Override public void addCookie(HttpCookie cookie) {cookies().put(cookie.getName(), cookie);}
	@Override public String reqcontentType() {return reqHd.getFirst("content-type");}
	@Override public String getContentType() {return resHd.getFirst("content-type");}
	@Override public void setContentType(String type) {resHd.set("content-type",type);}
	@Override public void setStatus(int status) {this.status=status;}
	@Override public boolean isCommitted() {return closed;}

	@Override
	public void sendError(int status, String msg) {
		var bytes=msg.getBytes();
		try {
			sendResponseHeaders(bytes.length);
			getResponseBody().write(bytes);
	        getRequestBody().close();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override public Object req() {return this;}
	@Override public Object res() {return this;}
	@Override public String getMethod() {return method;}
	@Override public void sattr(String name, Object value) {getSession().setAttribute(name, value);}
	@Override public Object sattr(String name) {return getSession().getAttribute(name);}
	@Override public Map<String,Object> sessionMap() {return getSession().attributes();}
	@Override public void setHeader(String name, String value) {resHd.set(name, value);}
	@Override public Map<String, Object> attr() {return getHttpContext().getAttributes();}
	@Override public int httpStatus() {return status;}
	@Override public String getRemoteAddr() {return connection.getSocket().getInetAddress().getHostAddress();}
	@Override public String getMapping(){if(mapping==null)mapping=Web.mapping(path, suffix, tbl());
	return mapping;}
	@Override public String getQueryString() {return query;}

	@Override
	public void sattr() {getSession().invalidate();}
	
}
